package com.bumptech.glide;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.AbsListView;

import com.bumptech.glide.request.target.BaseTarget;
import com.bumptech.glide.request.target.SizeReadyCallback;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.util.Synthetic;
import com.bumptech.glide.util.Util;

import java.util.List;
import java.util.Queue;

/**
 * Loads a few resources ahead in the direction of scrolling in any {@link AbsListView} so that
 * images are in the memory cache just before the corresponding view in created in the list. Gives
 * the appearance of an infinitely large image cache, depending on scrolling speed, cpu speed, and
 * cache size.
 *
 * <p> Must be put using
 * {@link AbsListView#setOnScrollListener(AbsListView.OnScrollListener)}, or have its
 * corresponding methods called from another {@link AbsListView.OnScrollListener} to
 * function. </p>
 *
 * @param <T> The type of the model being displayed in the list.
 */
public class ListPreloader<T> implements AbsListView.OnScrollListener {
    private final int maxPreload;
    private final PreloadTargetQueue preloadTargetQueue;
    private final RequestManager requestManager;
    private final PreloadModelProvider<T> preloadModelProvider;
    private final PreloadSizeProvider<T> preloadDimensionProvider;

    private int lastEnd;
    private int lastStart;
    private int lastFirstVisible = -1;
    private int totalItemCount;

    private boolean isIncreasing = true;

    /**
     * An implementation of PreloadModelProvider should provide all the models that should be
     * preloaded.
     *
     * @param <U> The type of the model being preloaded.
     */
    public interface PreloadModelProvider<U> {

        /**
         * Returns a {@link List} of models that need to be loaded for the list to display adapter items
         * in positions between {@code start} and {@code end}.
         *
         * <p>A list of any size can be returned so there can be multiple models per adapter position.
         *
         * <p>Every model returned by this method is expected to produce a valid {@link RequestBuilder}
         * in {@link #getPreloadRequestBuilder(Object)}. If that's not possible for any set of models,
         * avoid including them in the {@link List} returned by this method.
         *
         * <p>Although it's acceptable for the returned {@link List} to contain {@code null} models,
         * it's best to filter them from the list instead of adding {@code null} to avoid unnecessary
         * logic and expanding the size of the {@link List}
         *
         * @param position The adapter position.
         */
        @NonNull
        List<U> getPreloadItems(int position);

        /**
         * Returns a {@link RequestBuilder} for a given item on which
         * {@link RequestBuilder#load(Object)}} has been called or {@code null} if no valid load can be
         * started.
         *
         * <p>For the preloader to be effective, the {@link RequestBuilder} returned here must use
         * exactly the same size and set of options as the {@link RequestBuilder} used when the ``View``
         * is bound. You may need to specify a size in both places to ensure that the width and height
         * match exactly. If so, you can use
         * {@link com.bumptech.glide.request.RequestOptions#override(int, int)} to do so.
         *
         * <p>The target and context will be provided by the preloader.
         *
         * <p>If {@link RequestBuilder#load(Object)} is not called by this method, the preloader will
         * trigger a {@link RuntimeException}. If you don't want to load a particular item or position,
         * filter it from the list returned by {@link #getPreloadItems(int)}.
         *
         * @param item The model to load.
         */
        @Nullable
        RequestBuilder<?> getPreloadRequestBuilder(@NonNull U item);
    }

    /**
     * An implementation of PreloadSizeProvider should provide the size of the view in the list where
     * the resources will be displayed.
     *
     * @param <T> The type of the model the size should be provided for.
     */
    public interface PreloadSizeProvider<T> {

        /**
         * Returns the size of the view in the list where the resources will be displayed in pixels in
         * the format [x, y], or {@code null} if no size is currently available.
         *
         * <p>Note - The dimensions returned here must precisely match those of the view in the list.
         *
         * <p>If this method returns {@code null}, then no request will be started for the given item.
         *
         * @param item A model
         */
        @Nullable
        int[] getPreloadSize(@NonNull T item, int adapterPosition, int perItemPosition);
    }

    /**
     * Constructor for {@link ListPreloader} that accepts interfaces for providing
     * the dimensions of images to preload, the list of models to preload for a given position, and
     * the request to use to load images.
     *
     * @param preloadModelProvider     Provides models to load and requests capable of loading them.
     * @param preloadDimensionProvider Provides the dimensions of images to load.
     * @param maxPreload               Maximum number of items to preload.
     */
    public ListPreloader(@NonNull RequestManager requestManager,
                         @NonNull PreloadModelProvider<T> preloadModelProvider,
                         @NonNull PreloadSizeProvider<T> preloadDimensionProvider, int maxPreload) {
        this.requestManager = requestManager;
        this.preloadModelProvider = preloadModelProvider;
        this.preloadDimensionProvider = preloadDimensionProvider;
        this.maxPreload = maxPreload;
        preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1);
    }

    @Override
    public void onScrollStateChanged(AbsListView absListView, int scrollState) {
        // Do nothing.
    }

    @Override
    public void onScroll(AbsListView absListView, int firstVisible, int visibleCount,
                         int totalCount) {
        totalItemCount = totalCount;
        if (firstVisible > lastFirstVisible) {
            preload(firstVisible + visibleCount, true);
        } else if (firstVisible < lastFirstVisible) {
            preload(firstVisible, false);
        }
        lastFirstVisible = firstVisible;
    }

    private void preload(int start, boolean increasing) {
        if (isIncreasing != increasing) {
            isIncreasing = increasing;
            cancelAll();
        }
        preload(start, start + (increasing ? maxPreload : -maxPreload));
    }

    private void preload(int from, int to) {
        int start;
        int end;
        if (from < to) {
            start = Math.max(lastEnd, from);
            end = to;
        } else {
            start = to;
            end = Math.min(lastStart, from);
        }
        end = Math.min(totalItemCount, end);
        start = Math.min(totalItemCount, Math.max(0, start));

        if (from < to) {
            // Increasing
            for (int i = start; i < end; i++) {
                preloadAdapterPosition(preloadModelProvider.getPreloadItems(i), i, true);
            }
        } else {
            // Decreasing
            for (int i = end - 1; i >= start; i--) {
                preloadAdapterPosition(preloadModelProvider.getPreloadItems(i), i, false);
            }
        }

        lastStart = start;
        lastEnd = end;
    }

    private void preloadAdapterPosition(List<T> items, int position, boolean isIncreasing) {
        final int numItems = items.size();
        if (isIncreasing) {
            for (int i = 0; i < numItems; ++i) {
                preloadItem(items.get(i), position, i);
            }
        } else {
            for (int i = numItems - 1; i >= 0; --i) {
                preloadItem(items.get(i), position, i);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void preloadItem(@Nullable T item, int position, int perItemPosition) {
        if (item == null) {
            return;
        }
        int[] dimensions =
                preloadDimensionProvider.getPreloadSize(item, position, perItemPosition);
        if (dimensions == null) {
            return;
        }
        RequestBuilder<Object> preloadRequestBuilder =
                (RequestBuilder<Object>) preloadModelProvider.getPreloadRequestBuilder(item);
        if (preloadRequestBuilder == null) {
            return;
        }

        preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1]));
    }

    private void cancelAll() {
        for (int i = 0; i < maxPreload; i++) {
            requestManager.clear(preloadTargetQueue.next(0, 0));
        }
    }

    private static final class PreloadTargetQueue {
        private final Queue<PreloadTarget> queue;

        // The loop is short and the only point is to create the objects.
        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
        PreloadTargetQueue(int size) {
            queue = Util.createQueue(size);

            for (int i = 0; i < size; i++) {
                queue.offer(new PreloadTarget());
            }
        }

        public PreloadTarget next(int width, int height) {
            final PreloadTarget result = queue.poll();
            queue.offer(result);
            result.photoWidth = width;
            result.photoHeight = height;
            return result;
        }
    }

    private static final class PreloadTarget extends BaseTarget<Object> {
        @Synthetic
        int photoHeight;
        @Synthetic
        int photoWidth;

        @Synthetic
        PreloadTarget() {
        }

        @Override
        public void onResourceReady(@NonNull Object resource,
                                    @Nullable Transition<? super Object> transition) {
            // Do nothing.
        }

        @Override
        public void getSize(@NonNull SizeReadyCallback cb) {
            cb.onSizeReady(photoWidth, photoHeight);
        }

        @Override
        public void removeCallback(@NonNull SizeReadyCallback cb) {
            // Do nothing because we don't retain references to SizeReadyCallbacks.
        }
    }
}
